using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections.Generic;

using Open.Data.Persistence.Definitions;

namespace Open.Data.Persistence
{
    //Abstract representation of the class to generate the necessary script to define/redefine a database storage schema from types and attributes 
    public abstract class Deployment
    {
        //Property variables
        private Context _context;
        private DeploymentMode _deploymentMode;
        private DeploymentType _deploymentType;

        private Dictionary<string, Type> _typeNames;
        private bool _checkNames;
        
        #region Interface

        //Events
        public event DeployAssemblyHandler PreDeployAssembly;
        public event DeployAssemblyHandler PostDeployAssembly;

        public event PreValidateEventHandler PreValidateType;
        public event PostValidateEventHandler PostValidateType;
        public event DeployTypeExceptionHandler DeployTypeException;

        public event PreValidateKeyEventHandler PreValidateKey;
        public event PostValidateKeyEventHandler PostValidateKey;

        public event PreValidateReferenceEventHandler PreValidateReference;
        public event PostValidateReferenceEventHandler PostValidateReference;

        public event PreValidateIndexEventHandler PreValidateIndex;
        public event PostValidateIndexEventHandler PostValidateIndex;

        public event PreDeployTypeHandler PreDeployType;
        public event PostDeployTypeHandler PostDeployType;

        public event AddTableEventHandler AddTable;
        public event ChangeTableEventHandler ChangeTable;

        public event ExecuteScriptEventHandler ScriptExecute;

        //Constructors
        public Deployment()
        {
            DeploymentMode = DeploymentMode.Update;
            DeploymentType = DeploymentType.Validate;

            _typeNames = new Dictionary<string, Type>();
            CheckNames = true;
        }

        public Deployment(Context context): this()
        {
            Context = context;
        }

        //Properties
        public virtual Context Context
        {
            get
            {
                return _context;
            }
            set
            {
                _context = value;
            }
        }

        public virtual DeploymentMode DeploymentMode
        {
            get
            {
                return _deploymentMode;
            }
            set
            {
                _deploymentMode = value;
            }
        }

        public virtual DeploymentType DeploymentType
        {
            get
            {
                return _deploymentType;
            }
            set
            {
                _deploymentType = value;
            }
        }

        public virtual bool CheckNames
        {
            get
            {
                return _checkNames;
            }
            set
            {
                _checkNames = value;
            }
        }

        //Methods
        public abstract void CreateDatabase(string name);
        public abstract string[] GetDatabaseNames();
        public abstract void ExecuteScript(StringBuilder builder);
        public abstract string GetTableName(Type type);
        public abstract string SqlFromType(Type type);
        public abstract string DefaultFromType(Type type);
        public abstract bool Exists(Type type);
        public abstract bool HasRows(Type type);
        public abstract bool HasChanged(Type type, TableDefinition tableDefinition, KeyDefinition keyDefinition);

        public abstract TableDefinition GetTableDefinition(Type type);
        public abstract TableModification GetTableModification(Type type, TableDefinition definition);
        public abstract KeyDefinition GetKeyDefinition(Type type);

        public abstract void CreateTable(StringBuilder builder, Type type, string name);
        public abstract void AlterTable(StringBuilder builder, Type type, TableDefinition definition, TableModification modification);
        public abstract ValidationResult ValidateKey(StringBuilder builder, Type type, KeyDefinition keyDefinition);
        public abstract ValidationResult ValidateReference(StringBuilder builder, Type type, PropertyInfo[] properties, Type reftype);
        public abstract ValidationResult ValidateIndex(StringBuilder builder, Type type, PropertyInfo[] properties, IIndexAttribute attribute);
        public abstract void RemoveReferences(StringBuilder builder, Type type);
        public abstract void RemoveProcedures(StringBuilder builder, Type type);

        //Determines if a type is nullable
        public static bool GetTypeNullability(Type type)
        {
            if (typeof(IPersistable).IsAssignableFrom(type))
            {
                return true;
            }
            else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                return true;
            }
            else if (type == typeof(string))
            {
                return true;
            }
            else if (typeof(Stream).IsAssignableFrom(type))
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public static bool GetTypeConcurrency(Type type)
        {
            if (typeof(Stream).IsAssignableFrom(type))
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        public static bool IsDeployable(Type type)
        {
            bool ipers = typeof(IPersistable).IsAssignableFrom(type);
            bool view = typeof(IView).IsAssignableFrom(type);

            return (type.IsClass && typeof(IPersistable).IsAssignableFrom(type) && !typeof(IView).IsAssignableFrom(type) && !type.IsAbstract);
        }

        //Deploys an assembly to the context provided, making any changes necessary.
        public virtual void Deploy(Assembly assembly)
        {
            if (Context == null) throw new DeploymentException("Context has not been set. Set the Context property to provide a context to deploy to.");

            DeployAssemblyEventArgs e = new DeployAssemblyEventArgs(assembly, DeploymentMode, DeploymentType);
            OnPreDeployAssembly(this, e);

            if (!e.Cancel)
            {
                //Loop through and get all IPersistable, check if they have been created in this database
                Type[] deployableTypes = GetDeployableTypes(assembly.GetTypes());

                //Deploy each type and its primary key constraints
                foreach (Type type in deployableTypes)
                {
                    PreValidateEventArgs e2 = new PreValidateEventArgs(type);
                    OnPreValidateType(this, e2);

                    if (!e2.Cancel)
                    {
                        //Check if there is prefix/name clash
                        if (CheckNames)
                        {
                            Type existingType = null;
                            string typeName = Common.GetTypeName(type, Context.Prefix);

                            if (_typeNames.TryGetValue(typeName, out existingType))
                            {
                                if (type != existingType) throw new DeploymentException(string.Format("Could not deploy type {0} with prefix {1} becuase type {2} already uses the name {3}.", type.FullName, Context.Prefix, existingType.FullName, typeName));
                            }
                            else
                            {
                                //Add type to list 
                                _typeNames.Add(typeName, type);
                            }
                        }
                        
                        TableDefinition tableDefinition = null;
                        KeyDefinition keyDefinition = null;

                        bool exists = Exists(type);
                        bool changed = false;

                        //See if needs to be modified
                        if (exists)
                        {
                            tableDefinition = GetTableDefinition(type);
                            keyDefinition = GetKeyDefinition(type);

                            changed = HasChanged(type, tableDefinition, keyDefinition);
                        }

                        PostValidateEventArgs e3 = new PostValidateEventArgs(type);
                        e3.Changed = changed;
                        e3.Exists = exists;
                        OnPostValidateType(this, e3);

                        //Deploys a type with exception and event handling
                        if (!exists || changed || DeploymentMode == DeploymentMode.Overwrite) DeployType(type, tableDefinition, keyDefinition, exists);
                    }
                }

                //Validate the primary keys
                foreach (Type type in deployableTypes)
                {
                    if (Exists(type))
                    {
                        KeyDefinition keyDefinition = GetKeyDefinition(type);
                        DeployPrimaryKey(type, keyDefinition);
                    }
                }

                //Validate the foreign key references. All tables need to be built first.
                foreach (Type type in deployableTypes)
                {
                    DeployReferences(type);
                }

                //Deploy and validate the indexes
                foreach (Type type in deployableTypes)
                {
                    DeployIndexes(type);
                }

                OnPostDeployAssembly(this, e);
            }
        }

        protected virtual void OnPreDeployAssembly(object sender, DeployAssemblyEventArgs e)
        {
            if (PreDeployAssembly != null) PreDeployAssembly(sender, e);
        }
        protected virtual void OnPostDeployAssembly(object sender, DeployAssemblyEventArgs e)
        {
            if (PostDeployAssembly != null) PostDeployAssembly(sender, e);
        }

        protected virtual void OnPreValidateType(object sender, PreValidateEventArgs e)
        {
            if (PreValidateType != null) PreValidateType(sender ,e);
        }
        protected virtual void OnPostValidateType(object sender, PostValidateEventArgs e)
        {
            if (PostValidateType != null) PostValidateType(sender, e);
        }
        protected virtual void OnPreValidateKey(object sender, PreValidateKeyEventArgs e)
        {
            if (PreValidateKey != null) PreValidateKey(sender, e);
        }
        protected virtual void OnPostValidateKey(object sender, PostValidateKeyEventArgs e)
        {
            if (PostValidateKey != null) PostValidateKey(sender, e);
        }
        protected virtual void OnPreValidateReference(object sender, PreValidateReferenceEventArgs e)
        {
            if (PreValidateReference != null) PreValidateReference(sender, e);
        }
        protected virtual void OnPostValidateReference(object sender, PostValidateReferenceEventArgs e)
        {
            if (PostValidateReference != null) PostValidateReference(sender, e);
        }
        protected virtual void OnPreValidateIndex(object sender, PreValidateIndexEventArgs e)
        {
            if (PreValidateIndex != null) PreValidateIndex(sender, e);
        }
        protected virtual void OnPostValidateIndex(object sender, PostValidateIndexEventArgs e)
        {
            if (PostValidateIndex != null) PostValidateIndex(sender, e);
        }

        protected virtual void OnPreDeployType(object sender, PreDeployTypeEventArgs e)
        {
            if (PreDeployType != null) PreDeployType(sender, e);
        }
        protected virtual void OnPostDeployType(object sender, PostDeployTypeEventArgs e)
        {
            if (PostDeployType != null) PostDeployType(sender, e);
        }
        protected virtual void OnDeployTypeException(object sender, DeployTypeExceptionEventArgs e)
        {
            if (DeployTypeException != null) DeployTypeException(sender, e);
        }
        protected virtual void OnAddTable(object sender, AddTableEventInfo e)
        {
            if (AddTable != null) AddTable(sender, e);
        }
        protected virtual void OnChangeTable(object sender, ChangeTableEventInfo e)
        {
            if (ChangeTable != null) ChangeTable(sender, e);
        }
        protected virtual void OnScriptExecute(object sender, ExecuteScriptEventArgs e)
        {
            if (ScriptExecute != null) ScriptExecute(sender, e);
        }

        #endregion

        #region Implementation

        private void DeployType(Type type, TableDefinition tableDefinition, KeyDefinition keyDefinition, bool exists)
        {
            //Raise event, cancel if user cancels deployment
            string name = GetTableName(type);
            PreDeployTypeEventArgs e = new PreDeployTypeEventArgs(type, name);

            OnPreDeployType(this, e);

            if (e.Cancel) return;

            StringBuilder builder = new StringBuilder();

            //Alter or create table
            if (exists)
            {
                TableModification tableModification = GetTableModification(type, tableDefinition);
                OnChangeTable(this, new ChangeTableEventInfo(type, name, tableDefinition, keyDefinition, tableModification));

                RemoveReferences(builder, type);
                RemoveProcedures(builder, type);
                AlterTable(builder, type, tableDefinition, tableModification );
            }
            else
            {
                OnAddTable(this, new AddTableEventInfo(type, name, tableDefinition));
                CreateTable(builder, type, name);
            }

            //Deploy if required
            OnScriptExecute(this, new ExecuteScriptEventArgs(builder.ToString()));

            if (DeploymentType == DeploymentType.Deploy)
            {
                try
                {
                    ExecuteScript(builder);
                }
                catch (Exception ex)
                {
                    DeployTypeExceptionEventArgs args = new DeployTypeExceptionEventArgs(type, name, builder.ToString(), ex);
                    OnDeployTypeException(this, args);

                    //Setting the exception to null in the event stops the exception from being raised
                    if (args.Exception != null) throw new TypeDeploymentException("An error occurred deploying type " + type.FullName + ". " + ex.Message, ex);
                    
                    return;
                }
            }

            //Only create event object if events are added to the delegate
            PostDeployTypeEventArgs e2 = new PostDeployTypeEventArgs(type, name, builder.ToString());
            OnPostDeployType(this, e2);
        }

        private void DeployPrimaryKey(Type type, KeyDefinition keyDefinition)
        {
            CacheItem cache = Provider.Instance.ReflectionCache.Lookup(type);
            StringBuilder builder = new StringBuilder();

            PreValidateKeyEventArgs e = new PreValidateKeyEventArgs(type, keyDefinition);
   
            OnPreValidateKey(this, e);

            if (!e.Cancel)
            {
                ValidationResult result = ValidateKey(builder, type, keyDefinition);

                PostValidateKeyEventArgs e2 = new PostValidateKeyEventArgs(type);
                e2.Changed = result.Changed;
                e2.Exists = result.Exists;
                e2.Script = builder.ToString();

                OnPostValidateKey(this, e2);

                //Deploy if required
                OnScriptExecute(this, new ExecuteScriptEventArgs(builder.ToString()));

                if (DeploymentType == DeploymentType.Deploy) ExecuteScript(builder);
            }
        }

        private void DeployReferences(Type type)
        {
            CacheItem cache = Provider.Instance.ReflectionCache.Lookup(type);
            StringBuilder builder = new StringBuilder();

            ValidationResult result = null;
            PreValidateReferenceEventArgs e = null;
            PostValidateReferenceEventArgs e2 = null;

            //Validate that the keys in an inherited type reference the base type
            if (!cache.IsUnderlying && cache.BaseType != null && !cache.BaseType.IsAbstract)
            {
                e = new PreValidateReferenceEventArgs(type,cache.BaseType);
                OnPreValidateReference(this, e);
                
                if (!e.Cancel)
                {
                    result = ValidateReference(builder, type, cache.GetKeyProperties(), cache.BaseType);
                    
                    e2 = new PostValidateReferenceEventArgs(type, cache.BaseType);
                    e2.Changed = result.Changed;
                    e2.Exists = result.Exists;
                    e2.Script = builder.ToString();

                    OnPostValidateReference(this, e2);
                }
            }

            //Validate a foreign key to another table
            if (cache.HasForeignKeys)
            {
                int i = 0;
                foreach (PropertyInfo prop in cache.ForeignKeys)
                {
                    ForeignKeyAttribute attr = cache.ReferenceAttributes[i] as ForeignKeyAttribute;
                    if (attr != null)
                    {
                        e = new PreValidateReferenceEventArgs(type, attr.Reference);
                        OnPreValidateReference(this, e);

                        if (!e.Cancel)
                        {
                            result = ValidateReference(builder, type, new PropertyInfo[] { prop }, attr.Reference);

                            e2 = new PostValidateReferenceEventArgs(type, attr.Reference);
                            e2.Changed = result.Changed;
                            e2.Exists = result.Exists;
                            e2.Script = builder.ToString();

                            OnPostValidateReference(this, e2);
                        }
                    }
                    i++;
                }
            }

            //Check property references
            //Loop through each property          
            foreach (PropertyInfo prop in cache.Properties)
            {
                if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType))
                {
                    e = new PreValidateReferenceEventArgs(type, prop.PropertyType);
                    OnPreValidateReference(this, e);

                    if (!e.Cancel)
                    {
                        result = ValidateReference(builder, type, new PropertyInfo[] { prop }, prop.PropertyType);

                        e2 = new PostValidateReferenceEventArgs(type, prop.PropertyType);
                        e2.Changed = result.Changed;
                        e2.Exists = result.Exists;
                        e2.Script = builder.ToString();

                        OnPostValidateReference(this, e2);
                    }
                }
            }

            //Deploy if required
            OnScriptExecute(this, new ExecuteScriptEventArgs(builder.ToString()));

            if (DeploymentType == DeploymentType.Deploy) ExecuteScript(builder);
        }

        private void DeployIndexes(Type type)
        {
            CacheItem cache = Provider.Instance.ReflectionCache.Lookup(type);
            StringBuilder builder = new StringBuilder();

            ValidationResult result = null;
            PreValidateIndexEventArgs e = null;
            PostValidateIndexEventArgs e2 = null;

            int i = 0;

            foreach (PropertyInfo[] properties in cache.Indexes)
            {
                e = new PreValidateIndexEventArgs(type);
                OnPreValidateIndex(this, e);

                if (!e.Cancel)
                {
                     result = ValidateIndex(builder, type, properties, cache.IndexAttributes[i]);

                    e2 = new PostValidateIndexEventArgs(type);
                    e2.Changed = result.Changed;
                    e2.Exists = result.Exists;
                    e2.Script = builder.ToString();

                    OnPostValidateIndex(this, e2);
                }

                i++;
            }

            //Deploy if required
            OnScriptExecute(this, new ExecuteScriptEventArgs(builder.ToString()));

            if (i > 0 && DeploymentType == DeploymentType.Deploy) ExecuteScript(builder);
        }

        private Type[] GetDeployableTypes(Type[] types)
        {
            List<Type> results = new List<Type>(types.Length);

            foreach (Type type in types)
            {
                if (IsDeployable(type)) results.Add(type);
            }

            return results.ToArray();
        }

        #endregion
    }
}
